package com.wutong.resonance.model.audio.tool;

import com.wutong.resonance.model.audio.AudioData;
import com.wutong.resonance.model.audio.AudioParams;
import com.wutong.resonance.model.audio.enums.AudioType;
import com.wutong.resonance.model.audio.header.FileHeader;
import com.wutong.resonance.model.audio.header.WaveHeader;
import com.wutong.resonance.model.exception.AudioException;
import com.wutong.resonance.model.interfaces.PluginProcessor;
import com.wutong.resonance.model.tool.PluginQueue;
import com.wutong.resonance.model.tool.ThreadPool;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

import javax.sound.sampled.*;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;

/**
 * 音频录制类
 * <p>
 * 使用该类进行音频录制，支持流式和非流式两种录制方式。
 * <p>
 * 1. 录制音频分为流式写入音频文件和非流式写入音频文件两种方式，都使用 {@link #startRecording()} 和 {@link #stopRecording()} 方法控制录制的开始和结束。
 * <p>
 * 2. 需要手动指定录制音频的格式、采样率、通道数等参数，具体使用方法请参考 {@link AudioParams} 类。
 * <p>
 * 3. 录制的音频文件默认保存在 {@code System.getProperty("user.dir")} 目录下，文件名为 output_{@code Timestamp}.wav。可以通过 {@link Builder#setOutputAudioFileName(String)} 方法自定义文件名。
 * <p>
 * 4. 可以手动指定录制音频设备，如果不指定，则默认使用系统默认的录音设备。可以通过 {@link Builder#setDevice(Mixer.Info)} 方法指定录音设备。
 *
 * @author wutong
 * @since 1.0.0
 */
@Slf4j
@Setter
@Getter
public class AudioRecorder implements AutoCloseable, PluginProcessor {

    /**
     * 录音设备信息。
     */
    private Mixer.Info device;

    /**
     * 录音开始时间戳。
     */
    private long startRecordTime;

    /**
     * 录音结束时间戳。
     */
    private long stopRecordTime;

    /**
     * 录音状态，true 表示正在录音，false 表示未录音。
     */
    private volatile boolean isRecording = false;

    /**
     * 是否使用流式录音，true 表示使用流式录音，false 表示使用非流式录音。
     */
    private boolean isStreamRecording;

    /**
     * 音频参数，包括采样率、通道数、位深等。
     */
    private AudioParams audioParams;

    /**
     * 输出音频文件对象。
     */
    private AudioFile outputAudioFile;

    /**
     * 输出文件流。
     */
    private FileOutputStream outputStream;

    /**
     * 目标数据线，用于从音频输入设备读取音频数据。
     */
    private TargetDataLine targetDataLine;

    /**
     * 音频数据对象，用于存储录制的音频数据。
     */
    private AudioData audioData;

    /**
     * 线程池，用于执行异步任务。
     */
    private ThreadPool threadPool = ThreadPool.getInstance();

    /**
     * 缓冲区大小，用于存储从音频输入设备读取的音频数据。
     */
    private int bufferSize;

    /**
     * 缓冲区，用于存储从音频输入设备读取的音频数据。
     */
    private byte[] buffer;

    /**
     * 临时音频文件对象，用于存储流式录音的临时数据。
     */
    private AudioFile tmpAudioFile;

    /**
     * 临时输出文件流，用于写入流式录音的临时数据。
     */
    private FileOutputStream tmpOutputStream;

    private ByteArrayOutputStream outputStreamByteArray;

    private ByteArrayInputStream inputStreamByteArray;

    private final PluginQueue pluginQueue = new PluginQueue();

    /**
     * 构造方法，私有化，只能通过 {@link Builder} 构建对象。
     *
     * @param builder 构建器对象。
     */
    AudioRecorder(Builder builder) {
        this.audioParams = builder.audioParams;
        this.outputAudioFile = builder.outputAudioFile;
        this.tmpAudioFile = outputAudioFile.createTempFile();
        this.device = AudioSystem.getMixer(builder.device).getMixerInfo();
        this.isStreamRecording = builder.isStreamRecording;
        this.bufferSize = builder.bufferSize;
        log.info("AudioRecorder 初始化准备");
        init();
        log.info("AudioRecorder 初始化完成");
//        log.debug("AudioRecorder 信息：{}", this);
    }

    /**
     * 初始化录音器。
     * <p>
     * 该方法主要用于初始化录音器，包括设置音频参数、打开音频设备等。
     */
    private void init() {
        this.startRecordTime = 0;
        this.stopRecordTime = -1;
        log.debug("初始化录音时间");
        this.buffer = new byte[bufferSize];
        log.debug("初始化录音缓存");
        this.audioData = new AudioData(new byte[0], audioParams);
        log.debug("初始化录音数据");
        try {
            targetDataLine = (TargetDataLine) AudioSystem.getMixer(device).getLine(new Line.Info(TargetDataLine.class));
        } catch (LineUnavailableException e) {
            log.error("AudioRecorder 打开音频设备失败", e);
            throw new AudioException.AudioRecordException("AudioRecorder 打开音频设备失败", e);
        }
        log.info("载入音频设备：{}", device.getName());
    }

    @Override
    public PluginQueue getPluginQueue() {
        return pluginQueue;
    }

    /**
     * 启用录制音频。
     * <p>
     * 该方法用于录制音频，并将录制的音频数据保存到 {@link AudioData} 对象中。
     * <p>
     *
     * @param sync 是否阻塞主线程，true 则不阻塞，false 则阻塞。
     */
    public void startRecording(boolean sync) {
        if (isRecording) {
            log.warn("AudioRecorder 正在录制中，请勿重复调用 startRecording() 方法");
            return;
        }
        isRecording = true;
        Function<Void, Void> recordingTask = unused -> {
            try {
                targetDataLine.open(audioParams.toAudioFormat());
                targetDataLine.start();
                if (isStreamRecording) {
                    recordingWithStream();
                } else {
                    recordingWithoutStream();
                }
                return null;
            } catch (LineUnavailableException e) {
                log.error("AudioRecorder 打开音频设备失败", e);
                throw new AudioException.AudioRecordException("AudioRecorder 打开音频设备失败", e);
            } finally {
                isRecording = false;
            }
        };
        if (sync) {
            threadPool.execute(() -> recordingTask.apply(null));
        } else {
            recordingTask.apply(null);
        }
    }

    /**
     * 开始录制音频。
     * <p>
     * 该方法用于录制音频，并将录制的音频数据保存到 {@link AudioData} 对象中。
     * <p>
     * 该方法与 {@link #startRecording(boolean)} 方法的区别在于，该方法阻塞主线程，直到录制结束。
     */
    public void startRecording() {
        startRecording(false);
    }


    protected void stop() {
        if (!isRecording) {
            log.warn("AudioRecorder 未开始录制，请先调用 startRecording() 方法");
            return;
        }
        log.info("AudioRecorder 开始停止录制");
        isRecording = false;
        stopRecordTime = System.currentTimeMillis();
        log.info("AudioRecorder 录制结束，录制时长：{}ms", stopRecordTime - startRecordTime);
        log.info("AudioRecorder 录制完成");
    }

    /**
     * 停止录制音频。
     * <p>
     * 该方法用于停止录制音频，并将录制的音频数据保存到文件中。
     */
    public void stopRecording() {
        threadPool.execute(this::stop);
    }

    private void recording() throws IOException {
        int numBytesRead;
        while (isRecording) {
            numBytesRead = targetDataLine.read(buffer, 0, bufferSize);
            if (numBytesRead > 0) {
                audioData.switchTo(AudioData.DataType.BYTE);
                audioData.updateByteData(buffer);
                process(audioData);
                if (isStreamRecording) {
                    tmpOutputStream.write(audioData.getByteData(), 0, numBytesRead);
                } else {
                    outputStreamByteArray.write(audioData.getByteData(), 0, numBytesRead);
                }
            }
        }
    }


    /**
     * 使用流式录音方式录制音频。
     * <p>
     * 该方法用于使用流式录音方式录制音频，并将录制的音频数据保存到临时文件中。
     */
    protected void recordingWithStream() {
        log.info("AudioRecorder 开始流式录制");
        try {
            tmpOutputStream = new FileOutputStream(tmpAudioFile);
            log.debug("AudioRecorder 打开临时输出文件：{}", tmpAudioFile.getAbsolutePath());
            startRecordTime = System.currentTimeMillis();
            log.debug("开始录制");
            recording();
        } catch (IOException e) {
            log.error("AudioRecorder 执行录音失败", e);
            throw new AudioException.AudioRecordException("AudioRecorder 执行录音失败", e);
        } finally {
            try {
                if (tmpOutputStream != null) {
                    tmpOutputStream.flush();
                    tmpOutputStream.close();
                }
            } catch (IOException e) {
                log.error("关闭临时文件流失败", e);
            }
        }
    }

    /**
     * 使用非流式录音方式录制音频。
     * <p>
     * 该方法用于使用非流式录音方式录制音频，并将录制的音频数据保存到 {@link AudioData} 对象中。
     * <p>
     */
    protected void recordingWithoutStream() {
        log.info("AudioRecorder 开始非流式录制");
        try {
            outputStreamByteArray = new ByteArrayOutputStream();
            targetDataLine.start();
            startRecordTime = System.currentTimeMillis();
            log.info("开始录制");
            recording();
        } catch (IOException e) {
            log.error("AudioRecorder 执行录音失败", e);
            throw new AudioException.AudioRecordException("AudioRecorder 执行录音失败", e);
        } finally {
            try {
                if (outputStreamByteArray != null) {
                    outputStreamByteArray.flush();
                    outputStreamByteArray.close();
                }
            } catch (IOException e) {
                log.error("关闭输出流失败", e);
            }
        }
    }

    /**
     * 保存录制的音频文件。
     * <p>
     * 该方法用于将录制的音频数据保存到文件中。
     * <p>
     */
    public void save() {
        if (isStreamRecording) {
            saveWithStream();
        } else {
            saveWithoutStream();
        }
        log.info("AudioRecorder 保存录制文件：{}", outputAudioFile.getAbsolutePath());
        if (audioData == null) {
            log.warn("AudioRecorder 录制失败，未获取到录制数据");
        }
    }

    /**
     * 保存录制的音频文件。 (流式录制)
     * <p>
     * 该方法用于将录制的音频数据保存到文件中。
     */
    protected void saveWithStream() {
        log.debug("AudioRecorder - Stream 开始保存录音文件");
        try {
            tmpOutputStream.flush();
            tmpOutputStream.close();
            log.debug("AudioRecorder 关闭临时输出文件");

            outputStream = new FileOutputStream(outputAudioFile);
            FileHeader fileHeader = new WaveHeader((short) audioParams.getChannels(), audioParams.getSampleRate(), (short) audioParams.getBitDepth(), (int) tmpAudioFile.length());
            outputStream.write(fileHeader.getHeader());
            log.debug("AudioRecorder 写入文件头");
            try (InputStream inputStream = new FileInputStream(tmpAudioFile)) {
                byte[] buffer = new byte[4096];
                int bytesRead;
                while ((bytesRead = inputStream.read(buffer)) != -1) {
                    outputStream.write(buffer, 0, bytesRead);
                }
            }
            log.debug("AudioRecorder 写入音频数据");
            outputStream.flush();
            outputStream.close();
            log.debug("AudioRecorder 保存录音文件完成");

            outputAudioFile.deleteTempFile();
            tmpAudioFile = null;
            log.debug("AudioRecorder 删除临时文件");
        } catch (IOException e) {
            log.error("AudioRecorder 保存录音文件失败", e);
            throw new AudioException.AudioRecordException("AudioRecorder 保存录音文件失败", e);
        }
    }

    /**
     * 保存录制的音频文件。 (非流式录制)
     * <p>
     * 该方法用于将录制的音频数据保存到文件中。
     */
    protected void saveWithoutStream() {
        log.debug("AudioRecorder - WithoutStream 开始保存录音文件");
        try (ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStreamByteArray.toByteArray())) {
            outputStreamByteArray.flush();
            outputStreamByteArray.close();
            log.debug("AudioRecorder 关闭输出流");
            FileHeader fileHeader = new WaveHeader((short) audioParams.getChannels(), audioParams.getSampleRate(), (short) audioParams.getBitDepth(), (int) outputStreamByteArray.size());
            outputStream = new FileOutputStream(outputAudioFile);
            outputStream.write(fileHeader.getHeader());
            log.debug("AudioRecorder 写入Wave文件头");
            byte[] buffer = new byte[4096];
            int bytesRead;
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
            }
            log.debug("AudioRecorder 写入Wave音频数据");
            outputStream.flush();
            outputStream.close();
            log.debug("AudioRecorder 保存录音文件完成: {}", outputAudioFile.getAbsolutePath());
            outputStreamByteArray = null;
            log.debug("AudioRecorder 释放输出流");
        } catch (IOException e) {
            log.error("AudioRecorder 保存录音文件失败", e);
            throw new AudioException.AudioRecordException("AudioRecorder 保存录音文件失败", e);
        }
    }

    /**
     * 获取所有可用的录音设备。
     *
     * @return 所有可用的录音设备。
     */
    public static List<Mixer.Info> getAvailableRecordDevices() {
        List<Mixer.Info> availableDevices = new ArrayList<>();
        Mixer.Info[] mixerInfos = AudioSystem.getMixerInfo();
        for (Mixer.Info mixerInfo : mixerInfos) {
            Mixer mixer = AudioSystem.getMixer(mixerInfo);
            if (mixer.isLineSupported(new Line.Info(TargetDataLine.class))) {
                try {
                    TargetDataLine targetDataLine = (TargetDataLine) mixer.getLine(new Line.Info(TargetDataLine.class));
                    if (targetDataLine != null) {
                        availableDevices.add(mixerInfo);
                    }
                    if (targetDataLine != null) {
                        targetDataLine.close();
                    }
                } catch (Exception ignored) {
                }
            }
        }
        return availableDevices;
    }

    @Override
    public void close() {
        if (targetDataLine != null) {
            targetDataLine.close();
            log.info("AudioRecorder 关闭音频设备");
        }
        if (outputStream != null) {
            try {
                outputStream.flush();
                outputStream.close();
            } catch (IOException e) {
                log.error("关闭输出流失败", e);
            }
        }
        if (outputStreamByteArray != null) {
            try {
                outputStreamByteArray.flush();
                outputStreamByteArray.close();
            } catch (IOException e) {
                log.error("关闭输出流失败", e);
            }
        }
        if (tmpOutputStream != null) {
            try {
                tmpOutputStream.flush();
                tmpOutputStream.close();
            } catch (IOException e) {
                log.error("关闭临时文件流失败", e);
            }
            if (tmpAudioFile != null) {
                outputAudioFile.deleteTempFile();
            }
            log.info("AudioRecorder 已关闭");
        }
    }

    /**
     * 返回对象的字符串表示形式。
     *
     * @return 对象的字符串表示形式。
     * @apiNote 一般来说，{@code toString} 方法返回一个“以文本形式表示”此对象的字符串。结果应该是一个简明易懂地表示，易于人员阅读。建议所有子类都重写此方法。字符串输出在不同的时间或跨 JVM 调用时不一定稳定。
     * @implSpec 类 {@code Object} 的 {@code toString} 方法返回一个字符串，该字符串由对象的实例所属的类的名称、at 符号 `{@code @}` 和对象的哈希码的无符号十六进制表示组成。换句话说，该方法返回一个等于以下值表达式的字符串：
     * <blockquote>
     * <pre>
     * getClass().getName() + '@' + Integer.toHexString(hashCode())
     * </pre></blockquote>
     */
    @Override
    public String toString() {
        return "\n-----------------------\n" +
                "AudioRecorder 信息：\n" +
                "设备名称：" + device.getName() + "\n" +
                "设备描述：" + device.getDescription() + "\n" +
                "音频参数：" + audioParams + "\n" +
                "输出文件：" + outputAudioFile + "\n" +
                "是否流式录制：" + isStreamRecording + "\n" +
                "是否正在录制：" + isRecording + "\n" +
                "开始录制时间：" + startRecordTime + "\n" +
                "结束录制时间：" + stopRecordTime + "\n" +
                "-----------------------";
    }

    /**
     * {@link AudioRecorder} 的构建器类。
     */
    public static class Builder {
        /**
         * 录音设备信息。
         */
        private Mixer.Info device;

        /**
         * 是否使用流式录音，true 表示使用流式录音，false 表示使用非流式录音。
         */
        private boolean isStreamRecording = false;

        /**
         * 音频参数，包括采样率、通道数、位深等。
         */
        private final AudioParams audioParams;

        /**
         * 输出音频文件名，默认为 output_{@code Timestamp}。
         */
        private String outputAudioFileName = "output_" + System.currentTimeMillis();

        /**
         * 输出音频文件对象。
         */
        private AudioFile outputAudioFile;

        /**
         * 音频类型，默认为 WAV。
         */
        private AudioType audioType = AudioType.WAV;

        /**
         * 缓冲区大小，用于存储从音频输入设备读取的音频数据。
         */
        private int bufferSize = 1024;

        /**
         * 构造方法。
         *
         * @param audioParams 音频参数。
         */
        public Builder(AudioParams audioParams) {
            this.audioParams = audioParams;
        }

        /**
         * 构建 {@link AudioRecorder} 对象。
         *
         * @return {@link AudioRecorder} 对象。
         */
        public AudioRecorder build() {
            this.outputAudioFile = new AudioFile(new File(System.getProperty("user.dir"), outputAudioFileName + "." + audioType.getType()), audioType);
            return new AudioRecorder(this);
        }

        /**
         * 设置录音设备。
         *
         * @param device 录音设备信息。
         * @return 构建器对象。
         */
        public Builder setDevice(Mixer.Info device) {
            this.device = device;
            return this;
        }

        /**
         * 根据设备名称设置录音设备。
         *
         * @param deviceName 录音设备名称。
         * @return 构建器对象。
         * @throws IllegalArgumentException 如果找不到指定的录音设备。
         */
        public Builder setDevice(String deviceName) {
            for (Mixer.Info mixerInfo : getAvailableRecordDevices()) {
                if (mixerInfo.getName().contains(deviceName)) {
                    this.device = mixerInfo;
                    return this;
                }
            }
            throw new IllegalArgumentException("找不到指定的录音设备：" + deviceName);
        }

        /**
         * 根据设备索引设置录音设备。
         *
         * @param deviceIndex 录音设备索引。
         * @return 构建器对象。
         * @throws IllegalArgumentException 如果设备索引超出范围。
         */
        public Builder setDevice(int deviceIndex) {
            if (deviceIndex < 0 || deviceIndex >= getAvailableRecordDevices().size()) {
                throw new IllegalArgumentException("设备索引超出范围：" + deviceIndex);
            }
            this.device = getAvailableRecordDevices().get(deviceIndex);
            return this;
        }

        /**
         * 设置是否使用流式录音。
         *
         * @param isStreamRecording true 表示使用流式录音，false 表示使用非流式录音。
         * @return 构建器对象。
         */
        public Builder setStreamRecording(boolean isStreamRecording) {
            this.isStreamRecording = isStreamRecording;
            return this;
        }

        /**
         * 设置输出音频文件名。
         *
         * @param outputAudioFileName 输出音频文件名。
         * @return 构建器对象。
         */
        public Builder setOutputAudioFileName(String outputAudioFileName) {
            this.outputAudioFileName = outputAudioFileName;
            return this;
        }

        /**
         * 设置音频类型。
         *
         * @param audioType 音频类型。
         * @return 构建器对象。
         */
        public Builder setAudioType(AudioType audioType) {
            if (audioType != AudioType.WAV) {
                log.error("暂不支持其他音频类型：{}", audioType);
                throw new IllegalArgumentException("暂不支持其他音频类型：" + audioType);
            }
            this.audioType = audioType;
            return this;
        }

        /**
         * 设置缓冲区大小。
         *
         * @param bufferSize 缓冲区大小。
         * @return 构建器对象。
         */
        public Builder setBufferSize(int bufferSize) {
            this.bufferSize = bufferSize;
            return this;
        }
    }
}
